import re
from attributes import *
from errors import *
from shader import Shader
from stage import ShaderStage


class ShaderScript(object):
  ''' This class is used to parse the .shader files into shader objects. '''

  tokens = re.compile(r'''
    (?P<space>       \s         ) |
    (?P<comment>     //[^\n]*\n ) |
    (?P<open_brace>  \{         ) |
    (?P<close_brace> \}         ) |
    (?P<number>      -?\d*\.?\d+   ) |
    (?P<vector>      \(\s*(?P<x>\d*\.?\d+)\s+(?P<y>\d*\.?\d+)\s+(?P<z>\d*\.?\d+)\s*\) ) |
    (?P<quoted>      (?P<q>"|')[^\r\n]*[^\\](?P=q) ) |
    (?P<string>      [^\s]+     )
    ''', re.VERBOSE)

  @classmethod
  def filename_and_line(cls):
    return 'in %s at line %d' % (cls.filename, cls.buffer.count('\n', 0, cls.pos) + 1)

  @classmethod
  def parse(cls, buffer, filename='<unknown>', verbose=True):
    ''' Parse through the file, reading shaders one by one. '''
    cls.buffer = buffer
    cls.filename = filename
    cls.length = len(buffer)
    cls.pos = 0
    cls.verbose = verbose
    shaders = {}
    try:
      while True:
        shader = cls.parse_shader(cls.read_type('string'))
        shaders[shader.name] = shader
    except EndOfFileError:
      pass
    print "loaded %d shaders from %s" % (len(shaders), cls.filename)
    return shaders

  @classmethod
  def parse_error(cls, expected, found=None, token=None):
    s = 'expected %s' % expected.upper()
    if found != None:
      s += ' but found %s' % found.upper()
    if token != None:
      s += ' ("%s")' % token
    raise ParseError, s

  @classmethod
  def parse_shader(cls, name):
    ''' Parse a complete shader block and return the Shader object. '''
    shader = Shader(name)
    cls.read_type('open_brace')
    while True:
      token_type, token = cls.read()
      if token_type == 'close_brace':
        break
      elif token_type == 'open_brace':
        shader.stages.append(cls.parse_stage(shader))
        continue
      if isinstance(token, basestring):
        token = token.lower()
      try:
        if token == 'cull':
          shader.cull = CullSide(cls)
        elif token == 'deformvertexes':
          cls.read_deformvertexes(shader)
        elif token == 'fogparms':
          shader.fog = Fog(cls)
        elif token in ('nomipmap', 'nomipmaps'):
          shader.mipmap = False
          shader.picmip = False
        elif token == 'nopicmip':
          shader.picmip = False
        elif token == 'polygonoffset':
          shader.offset = True
        elif token == 'portal':
          shader.sort = Sort(value=Sort.PORTAL)
        elif token == 'skyparms':
          shader.sky = Sky(cls)
        elif token == 'sort':
          shader.sort = Sort(cls)
        elif token == 'surfaceparm':
          sp = SurfaceParameter(cls)
          shader.content_flags = getattr(shader, 'content_flags', 0) | sp.content_flags
          shader.surface_flags = getattr(shader, 'surface_flags', 0) | sp.surface_flags
        else:
          raise ParseWarning, "invalid token %s" % token
      except (ParseError, ParseWarning), pw:
        if cls.verbose and not token.startswith('q3map_') and not token.startswith('qer_') and not token == 'tesssize':
          print '%s: [%s] %s' % (cls.filename_and_line(), shader.name, str(pw))
        cls.read_to_eol()
    return shader

  @classmethod
  def parse_stage(cls, shader):
    ''' Parse a stage within a shader and return the ShaderStage object. '''
    stage = ShaderStage()
    while True:
      token_type, token = cls.read()
      if token_type == 'close_brace':
        break
      if isinstance(token, basestring):
        token = token.lower()
      try:
        if token == 'alphafunc':
          stage.alpha_func = AlphaFunc(cls)
        elif token == 'alphagen':
          stage.alpha_gen = AlphaGen(cls)
        elif token == 'animmap':
          stage.map = AnimMap(cls)
        elif token == 'blendfunc':
          stage.blend_func = BlendFunc(cls)
        elif token == 'clampmap':
          stage.map = Map(cls, clamp=True)
        elif token == 'depthfunc':
          stage.depth_func = DepthFunc(cls)
        elif token == 'depthwrite':
          stage.depth_write = True
        elif token == 'detail':
          stage.detail = True
        elif token == 'map':
          stage.map = Map(cls)
        elif token == 'rgbgen':
          stage.rgb_gen = RGBGen(cls)
        elif token == 'tcgen':
          stage.tc_gen = TCGen(cls)
        elif token == 'tcmod':
          stage.tc_mod = TCMod(cls)
        else:
          raise ParseWarning, "invalid token %s" % token
      except (ParseError, ParseWarning), pw:
        if cls.verbose:
          print '%s: [%s] %s' % (cls.filename_and_line(), shader.name, str(pw))
        cls.read_to_eol()
    return stage

  @classmethod
  def read(cls):
    '''
    Read an individual token. Comments and whitespace are skipped.
    - EndOfFileError is raised if EOF is reached before finding a token.
    - TokenError is raised if the parser encounters something it can't parse
      into a valid token.
    '''
    while True:
      if cls.pos >= cls.length:
        raise EndOfFileError
      m = cls.tokens.match(cls.buffer, cls.pos)
      if m is None:
        raise TokenError, '%s, column %d: invalid token' % (cls.filename_and_line(), cls.pos)
      cls.pos = m.end()
      token_type = m.lastgroup
      token = m.group(token_type)
      if token_type not in ('space', 'comment'):
        if token_type == 'number':
          token = float(token)
        elif token_type == 'quoted':
          token_type = 'string'
          token = token[1:-1]
        elif token_type == 'vector':
          token = [float(m.group('x')), float(m.group('y')), float(m.group('z'))]
        return token_type, token

  @classmethod
  def read_to_eol(cls):
    ''' Skip over everything up to the end of the line. '''
    initial_pos = cls.pos
    while True:
      if cls.pos >= cls.length:
        raise EndOfFileError
      if cls.buffer[cls.pos] == '\n':
        return cls.buffer[initial_pos:cls.pos]
      cls.pos += 1

  @classmethod
  def read_type(cls, type):
    '''
    Read the next token, and ensure that it is of the type specified, or raise
    ParseError if there is a type mismatch.
    '''
    token_type, token = cls.read()
    if token_type != type:
      cls.parse_error(expected=type, found=token_type, token=token)
    return token

  @classmethod
  def read_number(cls):
    return cls.read_type('number')

  @classmethod
  def read_string(cls):
    return cls.read_type('string')

  @classmethod
  def read_vector(cls):
    return cls.read_type('vector')

  @classmethod
  def read_deformvertexes(cls, shader):
    function = DeformMode(cls)
    map = {
      'AUTOSPRITE': AutoSpriteDeform,
      'AUTOSPRITE2': AutoSprite2Deform,
      'BULGE': BulgeDeform,
      'MOVE': MoveDeform,
      'NORMAL': NormalDeform,
      'WAVE': WaveDeform,
    }
    deform = None
    for k, c in map.iteritems():
      if function == getattr(DeformMode, k):
        deform = c(cls)
    if deform is None:
      raise ParseWarning, 'invalid deformvertexes type %s' % type
    if not hasattr(shader, 'deforms'):
      shader.deforms = []
    shader.deforms.append(deform)
